// Copyright 2003-2005 Arthur van Hoff Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jmdns.impl.constants.DNSRecordClass;
import javax.jmdns.impl.constants.DNSRecordType;

/**
 * A table of DNS entries. This is a map table which can handle multiple entries with the same name.
 * <p/>
 * Storing multiple entries with the same name is implemented using a linked list. This is hidden from the user and can
 * change in later implementation.
 * <p/>
 * Here's how to iterate over all entries:
 * 
 * <pre>
 *       for (Iterator i=dnscache.allValues().iterator(); i.hasNext(); ) {
 *             DNSEntry entry = i.next();
 *             ...do something with entry...
 *       }
 * </pre>
 * <p/>
 * And here's how to iterate over all entries having a given name:
 * 
 * <pre>
 *       for (Iterator i=dnscache.getDNSEntryList(name).iterator(); i.hasNext(); ) {
 *             DNSEntry entry = i.next();
 *           ...do something with entry...
 *       }
 * </pre>
 * @author Arthur van Hoff, Werner Randelshofer, Rick Blair, Pierre Frisch
 */
public class DNSCache extends AbstractMap<String, List<? extends DNSEntry>> {

   // private static Logger logger = Logger.getLogger(DNSCache.class.getName());

   private transient Set<Map.Entry<String, List<? extends DNSEntry>>> _entrySet = null;

   /**
     *
     */
   public static final DNSCache EmptyCache = new _EmptyCache();

   static final class _EmptyCache extends DNSCache {

      /**
       * {@inheritDoc}
       */
      @Override
      public int size() {
         return 0;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean isEmpty() {
         return true;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean containsKey(Object key) {
         return false;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean containsValue(Object value) {
         return false;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public List<DNSEntry> get(Object key) {
         return null;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public Set<String> keySet() {
         return Collections.emptySet();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public Collection<List<? extends DNSEntry>> values() {
         return Collections.emptySet();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public Set<Map.Entry<String, List<? extends DNSEntry>>> entrySet() {
         return Collections.emptySet();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean equals(Object o) {
         return (o instanceof Map) && ((Map<?, ?>) o).size() == 0;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public List<? extends DNSEntry> put(String key, List<? extends DNSEntry> value) {
         return null;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public int hashCode() {
         return 0;
      }

   }

   /**
     *
     */
   protected static class _CacheEntry extends Object implements Map.Entry<String, List<? extends DNSEntry>> {

      private List<? extends DNSEntry> _value;

      private String _key;

      /**
       * @param key
       * @param value
       */
      protected _CacheEntry(String key, List<? extends DNSEntry> value) {
         super();
         _key = (key != null ? key.trim().toLowerCase() : null);
         _value = value;
      }

      /**
       * @param entry
       */
      protected _CacheEntry(Map.Entry<String, List<? extends DNSEntry>> entry) {
         super();
         if (entry instanceof _CacheEntry) {
            _key = ((_CacheEntry) entry).getKey();
            _value = ((_CacheEntry) entry).getValue();
         }
      }

      /**
       * {@inheritDoc}
       */
      public String getKey() {
         return (_key != null ? _key : "");
      }

      /**
       * {@inheritDoc}
       */
      public List<? extends DNSEntry> getValue() {
         return _value;
      }

      /**
       * {@inheritDoc}
       */
      public List<? extends DNSEntry> setValue(List<? extends DNSEntry> value) {
         List<? extends DNSEntry> oldValue = _value;
         _value = value;
         return oldValue;
      }

      /**
       * Returns <tt>true</tt> if this list contains no elements.
       * @return <tt>true</tt> if this list contains no elements
       */
      public boolean isEmpty() {
         return this.getValue().isEmpty();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean equals(Object entry) {
         if (!(entry instanceof Map.Entry)) {
            return false;
         }
         return this.getKey().equals(((Map.Entry<?, ?>) entry).getKey()) && this.getValue().equals(((Map.Entry<?, ?>) entry).getValue());
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public int hashCode() {
         return (_key == null ? 0 : _key.hashCode());
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public synchronized String toString() {
         StringBuffer aLog = new StringBuffer(200);
         aLog.append("\n\t\tname '");
         aLog.append(_key);
         aLog.append("' ");
         if ((_value != null) && (!_value.isEmpty())) {
            for (DNSEntry entry : _value) {
               aLog.append("\n\t\t\t");
               aLog.append(entry.toString());
            }
         } else {
            aLog.append(" no entries");
         }
         return aLog.toString();
      }
   }

   /**
     *
     */
   public DNSCache() {
      this(1024);
   }

   /**
    * @param map
    */
   public DNSCache(DNSCache map) {
      this(map != null ? map.size() : 1024);
      if (map != null) {
         this.putAll(map);
      }
   }

   /**
    * Create a table with a given initial size.
    * @param initialCapacity
    */
   public DNSCache(int initialCapacity) {
      super();
      _entrySet = new HashSet<Map.Entry<String, List<? extends DNSEntry>>>(initialCapacity);
   }

   // ====================================================================
   // Map

   /*
    * (non-Javadoc)
    * @see java.util.AbstractMap#entrySet()
    */
   @Override
   public Set<Map.Entry<String, List<? extends DNSEntry>>> entrySet() {
      if (_entrySet == null) {
         _entrySet = new HashSet<Map.Entry<String, List<? extends DNSEntry>>>();
      }
      return _entrySet;
   }

   /**
    * @param key
    * @return map entry for the key
    */
   protected Map.Entry<String, List<? extends DNSEntry>> getEntry(String key) {
      String stringKey = (key != null ? key.trim().toLowerCase() : null);
      for (Map.Entry<String, List<? extends DNSEntry>> entry : this.entrySet()) {
         if (stringKey != null) {
            if (stringKey.equals(entry.getKey())) {
               return entry;
            }
         } else {
            if (entry.getKey() == null) {
               return entry;
            }
         }
      }
      return null;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public List<? extends DNSEntry> put(String key, List<? extends DNSEntry> value) {
      synchronized (this) {
         List<? extends DNSEntry> oldValue = null;
         Map.Entry<String, List<? extends DNSEntry>> oldEntry = this.getEntry(key);
         if (oldEntry != null) {
            oldValue = oldEntry.setValue(value);
         } else {
            this.entrySet().add(new _CacheEntry(key, value));
         }
         return oldValue;
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   protected Object clone() throws CloneNotSupportedException {
      return new DNSCache(this);
   }

   // ====================================================================

   /**
    * Returns all entries in the cache
    * @return all entries in the cache
    */
   public synchronized Collection<DNSEntry> allValues() {
      List<DNSEntry> allValues = new ArrayList<DNSEntry>();
      for (List<? extends DNSEntry> entry : this.values()) {
         if (entry != null) {
            allValues.addAll(entry);
         }
      }
      return allValues;
   }

   /**
    * Iterate only over items with matching name. Returns an list of DNSEntry or null. To retrieve all entries, one must
    * iterate over this linked list.
    * @param name
    * @return list of DNSEntries
    */
   public synchronized Collection<? extends DNSEntry> getDNSEntryList(String name) {
      Collection<? extends DNSEntry> entryList = this._getDNSEntryList(name);
      if (entryList != null) {
         entryList = new ArrayList<DNSEntry>(entryList);
      } else {
         entryList = Collections.emptyList();
      }
      return entryList;
   }

   private Collection<? extends DNSEntry> _getDNSEntryList(String name) {
      return this.get(name != null ? name.toLowerCase() : null);
   }

   /**
    * Get a matching DNS entry from the table (using isSameEntry). Returns the entry that was found.
    * @param dnsEntry
    * @return DNSEntry
    */
   public synchronized DNSEntry getDNSEntry(DNSEntry dnsEntry) {
      DNSEntry result = null;
      if (dnsEntry != null) {
         Collection<? extends DNSEntry> entryList = this._getDNSEntryList(dnsEntry.getKey());
         if (entryList != null) {
            for (DNSEntry testDNSEntry : entryList) {
               if (testDNSEntry.isSameEntry(dnsEntry)) {
                  result = testDNSEntry;
                  break;
               }
            }
         }
      }
      return result;
   }

   /**
    * Get a matching DNS entry from the table.
    * @param name
    * @param type
    * @param recordClass
    * @return DNSEntry
    */
   public synchronized DNSEntry getDNSEntry(String name, DNSRecordType type, DNSRecordClass recordClass) {
      DNSEntry result = null;
      Collection<? extends DNSEntry> entryList = this._getDNSEntryList(name);
      if (entryList != null) {
         for (DNSEntry testDNSEntry : entryList) {
            if (testDNSEntry.getRecordType().equals(type) && ((DNSRecordClass.CLASS_ANY == recordClass) || testDNSEntry.getRecordClass().equals(recordClass))) {
               result = testDNSEntry;
               break;
            }
         }
      }
      return result;
   }

   /**
    * Get all matching DNS entries from the table.
    * @param name
    * @param type
    * @param recordClass
    * @return list of entries
    */
   public synchronized Collection<? extends DNSEntry> getDNSEntryList(String name, DNSRecordType type, DNSRecordClass recordClass) {
      Collection<? extends DNSEntry> entryList = this._getDNSEntryList(name);
      if (entryList != null) {
         entryList = new ArrayList<DNSEntry>(entryList);
         for (Iterator<? extends DNSEntry> i = entryList.iterator(); i.hasNext();) {
            DNSEntry testDNSEntry = i.next();
            if (!testDNSEntry.getRecordType().equals(type) || ((DNSRecordClass.CLASS_ANY != recordClass) && !testDNSEntry.getRecordClass().equals(recordClass))) {
               i.remove();
            }
         }
      } else {
         entryList = Collections.emptyList();
      }
      return entryList;
   }

   /**
    * Adds an entry to the table.
    * @param dnsEntry
    * @return true if the entry was added
    */
   public synchronized boolean addDNSEntry(final DNSEntry dnsEntry) {
      boolean result = false;
      if (dnsEntry != null) {
         Map.Entry<String, List<? extends DNSEntry>> oldEntry = this.getEntry(dnsEntry.getKey());

         List<DNSEntry> aNewValue = null;
         if (oldEntry != null) {
            aNewValue = new ArrayList<DNSEntry>(oldEntry.getValue());
         } else {
            aNewValue = new ArrayList<DNSEntry>();
         }
         aNewValue.add(dnsEntry);

         if (oldEntry != null) {
            oldEntry.setValue(aNewValue);
         } else {
            this.entrySet().add(new _CacheEntry(dnsEntry.getKey(), aNewValue));
         }
         // This is probably not very informative
         result = true;
      }
      return result;
   }

   /**
    * Removes a specific entry from the table. Returns true if the entry was found.
    * @param dnsEntry
    * @return true if the entry was removed
    */
   public synchronized boolean removeDNSEntry(DNSEntry dnsEntry) {
      boolean result = false;
      if (dnsEntry != null) {
         Map.Entry<String, List<? extends DNSEntry>> existingEntry = this.getEntry(dnsEntry.getKey());
         if (existingEntry != null) {
            result = existingEntry.getValue().remove(dnsEntry);
            // If we just removed the last one we need to get rid of the entry
            if (existingEntry.getValue().isEmpty()) {
               this.entrySet().remove(existingEntry);
            }
         }
      }
      return result;
   }

   /**
    * Replace an existing entry by a new one.<br/>
    * <b>Note:</b> the 2 entries must have the same key.
    * @param newDNSEntry
    * @param existingDNSEntry
    * @return <code>true</code> if the entry has been replace, <code>false</code> otherwise.
    */
   public synchronized boolean replaceDNSEntry(DNSEntry newDNSEntry, DNSEntry existingDNSEntry) {
      boolean result = false;
      if ((newDNSEntry != null) && (existingDNSEntry != null) && (newDNSEntry.getKey().equals(existingDNSEntry.getKey()))) {
         Map.Entry<String, List<? extends DNSEntry>> oldEntry = this.getEntry(newDNSEntry.getKey());

         List<DNSEntry> aNewValue = null;
         if (oldEntry != null) {
            aNewValue = new ArrayList<DNSEntry>(oldEntry.getValue());
         } else {
            aNewValue = new ArrayList<DNSEntry>();
         }
         aNewValue.remove(existingDNSEntry);
         aNewValue.add(newDNSEntry);

         if (oldEntry != null) {
            oldEntry.setValue(aNewValue);
         } else {
            this.entrySet().add(new _CacheEntry(newDNSEntry.getKey(), aNewValue));
         }
         // This is probably not very informative
         result = true;
      }
      return result;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public synchronized String toString() {
      StringBuffer aLog = new StringBuffer(2000);
      aLog.append("\t---- cache ----");
      for (Map.Entry<String, List<? extends DNSEntry>> entry : this.entrySet()) {
         aLog.append("\n\t\t");
         aLog.append(entry.toString());
      }
      return aLog.toString();
   }

}
